iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 28 篇

Day 28:遭遇重寫與重構(Refactoring):當 AI 生成的程式碼變成 Technical Debt 時的解決方案

  • 分享至 

  • xImage
  •  

今日目標

經過前面 27 天的努力,我們的 AI 個人財務追蹤器已經順利上雲,並裝上了 Sentry 與 Logtail 防禦天眼。然而,隨著專案功能越疊越多,昨天在 Sentry 儀表板上出現的幾筆零星報錯,暴露出了一個所有 Vibe Coder 都無法迴避的硬傷——技術債(Technical Debt)爆發。

在 Vibe Coding 模式下,我們習慣用自然語言快速下 Prompt 讓 AI 生成程式碼。AI 為了最快達成「功能可執行」的目標,常常會大量複製貼上類似的 API 請求、將 State 混亂散落於數個組件間、或是寫出長達 500 行的「巨石型組件(God Component)」。

今天我們的目標是對專案進行大刀闊斧的技術債清理與重構:

  1. 識別 AI 技術債典型症狀:Prop Drilling、重複 Fetching 邏輯、State 競爭條件(Race Condition)與硬編碼邏輯。
  2. 運用 AI 協助「安全重構(Safe Refactoring)」:藉由 Day 10 與 Day 18 建立的自動化測試網,引導 AI 進行抽象化重構,絕不破壞既有功能。

Human vs. AI 實戰記錄

當你直接對 AI 說「請幫我重構這段程式碼」時,AI 最喜歡發揮「想像力」,自作主張地刪除功能、更換套件甚至重新發明輪子。要讓 AI 成為重構神隊友,我們必須給予明確的設計模式(Design Pattern)與重構目標。


1. 給 AI 的指令 (Prompt)

我們選定了專案中最混亂的 FinancialDashboard.tsx,這段程式碼原本包含了 10 個 useState、3 個 useEffect,並在前端手動過濾財務紀錄,導致畫面卡頓且極難維護。

你是一位資深 React & TypeScript 架構師。
我們要對 `FinancialDashboard.tsx` 進行重構,以清除 AI 早期生成程式碼累積的技術債。

重構目標與約束條件:
1. **抽離狀態管理**:將原本散落在元件內的 10 個 `useState` 與資料獲取邏輯,重構為自訂 Hook `useFinancialData`(結合 TanStack Query / SWR)。
2. **單一職責原則 (SRP)**:將原本 400 行的巨石組件,拆分為 `<StatCardsSection />`、`<ExpenseChartSection />` 與 `<TransactionTableSection />` 獨立子組件。
3. **消除 Redundant Rendering**:使用 `useMemo` 與 `useCallback` 優化高頻率 Filter/Sort 計算。
4. **絕對限制**:不可變更任何 UI 外觀與既有功能,必須確保所有型別嚴格對齊,不要引入新的第三方庫。


2. AI 產出的結果 (Code)

AI 生成了重構後的 Custom Hook 與組件結構:

// AI 生成的 Custom Hook (hooks/useFinancialData.ts)
import { useState, useEffect } from 'react';

export function useFinancialData() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [filter, setFilter] = useState('all');

  useEffect(() => {
    fetch(`/api/transactions?filter=${filter}`)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, [filter]);

  return { data, loading, filter, setFilter };
}

// AI 生成的重構後主元件 (FinancialDashboard.tsx)
import { useFinancialData } from '@/hooks/useFinancialData';
import { StatCardsSection } from './StatCardsSection';
import { ExpenseChartSection } from './ExpenseChartSection';

export function FinancialDashboard() {
  const { data, loading, filter, setFilter } = useFinancialData();

  if (loading) return <div>Loading...</div>;

  return (
    <div className="space-y-6">
      <StatCardsSection data={data} />
      <ExpenseChartSection data={data} />
    </div>
  );
}


3. 修正與翻車紀錄 (Debug Experience)

AI 的重構乍看之下完成了「檔案拆分」,但一放到真實環境執行,立刻陷入了經典的「越重構越爛」Vibe Coding 泥沼!

翻車點一:用舊的 Fetch 替換了 TanStack Query,遺失快取與 Race Condition 防禦

專案在 Day 15 時已經引進了 TanStack Query (React Query) 來處理前後端 API 快取與自動 Revalidate。AI 在重構 Hook 時,居然「退步」用回了最原始的 useEffect + fetch!這導致使用者切換 Filter 時,若舊請求比較晚回傳,會產生狀態覆蓋(Race Condition),讓頁面呈現錯誤數據!

翻車點二:盲目拆分組件引發 Prop Drilling 與無限重渲染 (Infinite Rerender)

AI 雖然建立了 <StatCardsSection/>,但為了傳遞使用者點擊事件,直接把 setFilter、data、refetch 一路往下層傳了四層(Prop Drilling)。更糟的是,AI 忘記用 useCallback 包裹子組件傳遞的 Handler,導致每次父組件狀態一變,所有子圖表與表格全部強制重新渲染!

介入修復!

我們再次下達精確指示,規範 AI 必須基於現代 React Best Practices 重構:

  1. 維持 TanStack Query 架構:保留自動快取、背景更新與 Race Condition 取消機制。
  2. 抽象化語意清析的 Selector:在 Custom Hook 內部使用 useMemo 完成資料篩選與金額加總,不將計算負擔留給 UI 組件。
  3. 組件邊界隔離(Component Boundary):使用 React.memo 搭配純粹的 Props 介面,阻斷不必要的子組件 Re-render。

修正後的重構程式碼:

hooks/useFinancialData.ts(強健的 Custom Hook):

import { useQuery } from '@tanstack/react-query';
import { useMemo, useState, useCallback } from 'react';
import { Transaction } from '@prisma/client';

export function useFinancialData() {
  const [selectedCategory, setSelectedCategory] = useState<string>('ALL');

  // 1. 保留 React Query 帶來的快取與 Race Condition 防禦
  const { data: transactions = [], isLoading, isError, refetch } = useQuery<Transaction[]>({
    queryKey: ['transactions'],
    queryFn: async () => {
      const res = await fetch('/api/transactions');
      if (!res.ok) throw new Error('Failed to fetch transactions');
      return res.json();
    },
    staleTime: 1000 * 60 * 5, // 5 分鐘內維持 fresh 狀態
  });

  // 2. 集中複雜的資料轉換與統計計算 (避免在 UI 組件中重複計算)
  const filteredTransactions = useMemo(() => {
    if (selectedCategory === 'ALL') return transactions;
    return transactions.filter(t => t.category === selectedCategory);
  }, [transactions, selectedCategory]);

  const summary = useMemo(() => {
    return filteredTransactions.reduce(
      (acc, t) => {
        if (t.type === 'INCOME') acc.totalIncome += t.amount;
        if (t.type === 'EXPENSE') acc.totalExpense += t.amount;
        return acc;
      },
      { totalIncome: 0, totalExpense: 0 }
    );
  }, [filteredTransactions]);

  const handleCategoryChange = useCallback((category: string) => {
    setSelectedCategory(category);
  }, []);

  return {
    transactions: filteredTransactions,
    summary,
    isLoading,
    isError,
    selectedCategory,
    handleCategoryChange,
    refetch,
  };
}

components/dashboard/FinancialDashboard.tsx(乾淨俐落的主組件):

'use client';

import { useFinancialData } from '@/hooks/useFinancialData';
import { StatCardsSection } from './StatCardsSection';
import { ExpenseChartSection } from './ExpenseChartSection';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';

export function FinancialDashboard() {
  const {
    transactions,
    summary,
    isLoading,
    isError,
    selectedCategory,
    handleCategoryChange,
    refetch,
  } = useFinancialData();

  if (isLoading) return <DashboardSkeleton />;
  if (isError) return <DashboardErrorState onRetry={refetch} />;

  return (
    <div className="space-y-8 p-6 max-w-7xl mx-auto">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold tracking-tight">財務儀表板</h1>
        <Button variant="outline" size="sm" onClick={() => refetch()}>
          <RefreshCw className="h-4 w-4 mr-2" /> 重新整理
        </Button>
      </div>

      {/* 數據卡片區域 */}
      <StatCardsSection summary={summary} />

      {/* 圖表與動態表格區域 */}
      <ExpenseChartSection 
        transactions={transactions} 
        currentCategory={selectedCategory}
        onSelectCategory={handleCategoryChange}
      />
    </div>
  );
}


學習小結

  1. AI 時代的「技術債」來得更快,也更隱蔽:
    在傳統開發模式下,技術債是慢慢累積的;但在 Vibe Coding 模式下,AI 可以幾秒鐘生成數百行程式碼,如果你只看「功能會跑」就 Pass,技術債會以 10 倍速度侵蝕你的專案。定期進行重構(Refactoring Cycle)是維持 AI 專案可維護性的唯一解法。
  2. 重構時,測試網(Test Suite)是你的生命線:
    今天我們敢對 400 行的巨石組件進行切碎重構,完全仰賴 Day 10 (Jest) 與 Day 18 (Playwright) 建立的自動化測試。重構完畢後指令一打:npm run test 全綠過關,這才是真正的「Vibe Mode」安全重構!
  3. 人腦的價值在於「設計模式 (Design Patterns)」的掌舵:
    AI 擅長處理區域性的程式碼編寫,但在「狀態要放在哪裡(State Colocation)」、「何時該用 Custom Hook 封裝」、「如何阻斷不必要的 Re-render」等架構決策上,必須由人類工程師下達精確的語意規範。

上一篇
Day 27:監控與 Log 系統:整合 Sentry / Logtail,建立發布後的錯誤監控
下一篇
# Day 29:專案成果展示:成品展示、系統成果展示與展示頁面(Landing Page)製作
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言